package com.fashiontextile.gateway.filter;

import com.alibaba.nacos.common.utils.StringUtils;
import com.fashiontextile.common.constant.TokenConstants;
import com.fashiontextile.common.entity.LoginUser;
import com.fashiontextile.common.exception.AuthenticationException;
import com.fashiontextile.common.utils.JwtUtils;
import com.fashiontextile.common.utils.RedisUtils;
import com.fashiontextile.gateway.config.properties.AuthSecurityRulesProperties;
import com.nimbusds.jose.JOSEException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author brooke_zb
 */
@Slf4j
@Component
@AllArgsConstructor
@EnableConfigurationProperties(AuthSecurityRulesProperties.class)
public class AuthFilter implements GlobalFilter, Ordered {
    private static final AntPathMatcher matcher = new AntPathMatcher();
    private AuthSecurityRulesProperties properties;
    private RedisUtils redisUtils;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) throws AuthenticationException {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest.Builder mutate = request.mutate();
        String path = request.getURI().getPath();

        // 获取令牌
        String jwt = getToken(request);

        // 白名单免认证
        for (String pattern : properties.getWhiteList()) {
            if (matcher.match(pattern, path)) {
                // 清除参数，添加白名单头并放行
                mutate.headers(httpHeaders -> {
                    // 这里删了无法退出登录
                    if (jwt != null) {
                        httpHeaders.set(TokenConstants.TOKEN_HEADER, jwt);
                    }
                    httpHeaders.remove("Cookie");
                    httpHeaders.remove(TokenConstants.ROLE_HEADER);
                    httpHeaders.add(TokenConstants.WHITE_LIST_HEADER, TokenConstants.WHITE_LIST_VALUE);
                });
                return chain.filter(exchange.mutate().request(mutate.build()).build());
            }
        }

        // 黑名单直接阻拦
        for (String pattern : properties.getBlackList()) {
            if (matcher.match(pattern, path)) {
                return unAuth(exchange.getResponse(), "您没有权限访问该链接");
            }
        }

        // 令牌为空直接阻拦
        if (jwt == null) {
            return unAuth(exchange.getResponse(), "令牌不能为空");
        }

        // 解析令牌
        Map<String, Object> payload = null;
        try {
            payload = JwtUtils.parseToken(jwt.trim());
        } catch (ParseException e) {
            log.warn("令牌格式错误", e);
        } catch (JOSEException e) {
            log.warn("令牌非法，签名验证不通过", e);
        }

        // 校验令牌
        assert payload != null;
        String token = (String) payload.get("token");
        Long uid = (Long) payload.get("uid");

        String createTime = redisUtils.getStringValue(TokenConstants.REDIS_PREFIX + uid + ":" + token);
        LoginUser user = (LoginUser) redisUtils.getObjectValue(TokenConstants.REDIS_PREFIX + uid);
        if (createTime == null) {
            return unAuth(exchange.getResponse(), "登录状态已过期");
        }

        // 检测令牌剩余时长，并无感刷新
        long duration = TimeUnit.MILLISECONDS.toMinutes(Long.parseLong(createTime) +
                TimeUnit.MINUTES.toMillis(TokenConstants.EXPIRATION) -
                System.currentTimeMillis());
        if (duration < TokenConstants.REFRESH_DURATION) {
            refreshToken(token, uid);
            response.addCookie(ResponseCookie.from(TokenConstants.TOKEN_HEADER, jwt)
                    .maxAge(TimeUnit.MINUTES.toSeconds(TokenConstants.EXPIRATION))
                    .path("/")
                    .build());
        }

        // 清除参数，添加角色头并放行
        mutate.headers(httpHeaders -> {
            httpHeaders.remove("Cookie");
            httpHeaders.remove(TokenConstants.TOKEN_HEADER);
            httpHeaders.remove(TokenConstants.WHITE_LIST_HEADER);
            try {
                httpHeaders.set(TokenConstants.ROLE_HEADER, URLEncoder.encode(user.getRole(), String.valueOf(StandardCharsets.UTF_8)));
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            httpHeaders.set(TokenConstants.LOGIN_UID_HEADER, uid.toString());
        });
        return chain.filter(exchange.mutate().request(mutate.build()).response(response).build());
    }

    /**
     * 获取请求token
     *
     * @return jwt
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(TokenConstants.TOKEN_HEADER);

        // 若获取不到则从cookie获取
        if (StringUtils.isEmpty(token)) {
            HttpCookie cookie = request.getCookies().getFirst(TokenConstants.TOKEN_HEADER);
            if (cookie == null) {
                return null;
            }
            token = cookie.getValue();
        }

        // 如果前端设置了令牌前缀，则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }

        return token;
    }

    /**
     * 未授权返回401
     *
     * @param response 请求响应
     * @param msg      返回消息
     */
    private Mono<Void> unAuth(ServerHttpResponse response, String msg) {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        DataBuffer buffer = response.bufferFactory().wrap(("{\"success\":false,\"data\":null,\"msg\":\"" + msg + "\"}").getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(buffer));
    }

    private void refreshToken(String token, Long uid) {
        redisUtils.setStringValue(TokenConstants.REDIS_PREFIX + uid + ":" + token,
                String.valueOf(System.currentTimeMillis()),
                TokenConstants.EXPIRATION,
                TimeUnit.MINUTES);
    }

    @Override
    public int getOrder() {
        return -50;
    }
}